#include "JScriptConsole.h"
#include "JScriptHighlighter.h"
#include "JLog.h"

#include <QMenu>
#include <QMouseEvent>
#include <QKeyEvent>
#include <QApplication>
#include <QTextDocumentFragment>
#include <QTextBlock>
#include <QTextCursor>
#include <QDebug>
#include <QCompleter>
#include <QStringListModel>
#include <QScrollBar>
#include <QPainter>

JScriptConsole::JScriptConsole(QWidget *parent, const PythonQtObjectPtr& context, Qt::WindowFlags windowFlags)
	: QTextEdit(parent)
{
	setWindowFlags(windowFlags);
	setMouseTracking(true);
	setTabStopWidth(40);
	setWordWrapMode(QTextOption::NoWrap);
	//setFrameShape(QFrame::Panel);
	//setFrameShadow(QFrame::Sunken);

	QFont font;
	font.setFamily(QString::fromUtf8("Courier New"));
	font.setPointSize(9);
	setFont(font);

	mDefaultTextCharacterFormat = currentCharFormat();
	mContext                    = context;
	mHistoryPosition            = 0;

	mCompleter = new QCompleter(this);
	mCompleter->setWidget(this);
	QObject::connect(mCompleter, SIGNAL(activated(const QString&)),
		this, SLOT(insertCompletion(const QString&)));

	clear();

	connect(PythonQt::self(), SIGNAL(pythonStdOut(const QString&)), this, SLOT(stdOut(const QString&)));
	connect(PythonQt::self(), SIGNAL(pythonStdErr(const QString&)), this, SLOT(stdErr(const QString&)));
	connect(this, SIGNAL(cursorPositionChanged()), this, SLOT(slotCursorPositionChanged()));
	connect(horizontalScrollBar(), SIGNAL(valueChanged(int)),this, SLOT(slotViewportUpdate()));

	JScriptHighlighter *highlighter = new JScriptHighlighter(document());

	//execute the lines or files after app launch

	//mContext.evalScript("import sys");
	
	mContext.evalFile("init.py");
	Log.Post("compile_ init.py", JLOG_SUCCESS);
	
	flushStdOut();
	appendCommandPrompt();
}

JScriptConsole::~JScriptConsole()
{

}

void JScriptConsole::stdOut(const QString& s)
{
	mStdOut += s;
	int idx;
	while ((idx = mStdOut.indexOf('\n'))!=-1) {
		setTextColor(QColor(0,0,255));
		consoleMessage(mStdOut.left(idx));
		std::cout << mStdOut.left(idx).toLatin1().data() << std::endl;
		mStdOut = mStdOut.mid(idx+1);
	}
}

void JScriptConsole::stdErr(const QString& s)
{
	mStdErr += s;
	int idx;
	while ((idx = mStdErr.indexOf('\n'))!=-1) {
		setTextColor(QColor(255,0,0));
		consoleMessage(mStdErr.left(idx));
		std::cout << mStdErr.left(idx).toLatin1().data() << std::endl;
		mStdErr = mStdErr.mid(idx+1);
	}
}

void JScriptConsole::flushStdOut()
{
	if (!mStdOut.isEmpty()) {
		stdOut("\n");
	}
	if (!mStdErr.isEmpty()) {
		stdErr("\n");
	}
}

void JScriptConsole::clear() {

	QTextEdit::clear();
	appendCommandPrompt();
}

void JScriptConsole::executeLine()
{
	QTextCursor textCursor = this->textCursor();
	textCursor.movePosition(QTextCursor::End);

	// Select the text from the command prompt until the end of the block
	// and get the selected text.
	textCursor.setPosition(commandPromptPosition());
	textCursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
	QString code = textCursor.selectedText();

	// i don't know where this trailing space is coming from, blast it!
	if (code.endsWith(" ")) {
		code.truncate(code.length()-1);
	}

	if (!code.isEmpty()) {
		// Update the history
		mHistory << code;
		mHistoryPosition = mHistory.count();
		mCurrentMultiLineCode += code + "\n";
		executeCode(mCurrentMultiLineCode);
		mCurrentMultiLineCode = "";
	}
	// Insert a new command prompt
	appendCommandPrompt();

}

void JScriptConsole::executeCode(const QString& code)
{
	// put visible cursor to the end of the line
	QTextCursor cursor = QTextEdit::textCursor();
	cursor.movePosition(QTextCursor::End);
	setTextCursor(cursor);

	int cursorPosition = this->textCursor().position();

	// evaluate the code
	mStdOut = "";
	mStdErr = "";
	PythonQtObjectPtr p;
	p.setNewRef(PyRun_String(code.toLatin1().data(), Py_single_input, PyModule_GetDict(mContext), PyModule_GetDict(mContext)));
	if (!p) {
		PythonQt::self()->handleError();
	}
	// alternative way
	//mContext.evalScript(code,Py_single_input);

	flushStdOut();

	//bool messageInserted = (this->textCursor().position() != cursorPosition);

	// If a message was inserted, then put another empty line before the command prompt
	// to improve readability.
	/*if (messageInserted) {
		append(QString());
	}*/
}

void JScriptConsole::appendCommandPrompt()
{
	mCommandPrompt = ">>> ";
	append(mCommandPrompt);

	QTextCursor cursor = textCursor();
	cursor.movePosition(QTextCursor::End);
	setTextCursor(cursor);
}

void JScriptConsole::setCurrentFont(const QColor& color, bool bold) {

	QTextCharFormat charFormat(mDefaultTextCharacterFormat);

	QFont font(charFormat.font());
	font.setBold(bold);
	charFormat.setFont(font);

	QBrush brush(charFormat.foreground());
	brush.setColor(color);
	charFormat.setForeground(brush);

	setCurrentCharFormat(charFormat);
}

int JScriptConsole::commandPromptPosition() {

	QTextCursor textCursor(this->textCursor());
	textCursor.movePosition(QTextCursor::End);

	return textCursor.block().position() + mCommandPrompt.length();
}

void JScriptConsole::insertCompletion(const QString& completion)
{
	QTextCursor tc = textCursor();
	tc.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor);
	if (tc.selectedText()==".") {
		tc.insertText(QString(".") + completion);
	} else {
		tc = textCursor();
		tc.movePosition(QTextCursor::StartOfWord, QTextCursor::MoveAnchor);
		tc.movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor);
		tc.insertText(completion);
		setTextCursor(tc);
	}
}

void JScriptConsole::handleCompleterMenu()
{
	QTextCursor textCursor   = this->textCursor();
	int pos = textCursor.position();
	textCursor.setPosition(commandPromptPosition());
	textCursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
	int startPos = textCursor.selectionStart();

	int offset = pos-startPos;
	QString text = textCursor.selectedText();

	QString textToComplete;
	int cur = offset;
	while (cur--) {
		QChar c = text.at(cur);
		if (c.isLetterOrNumber() || c == '.' || c == '_') {
			textToComplete.prepend(c);
		} 
		else {
			break;
		}
	}

	QString lookup;
	QString compareText = textToComplete;
	int dot = compareText.lastIndexOf('.');
	if (dot!=-1) {
		lookup = compareText.mid(0, dot);
		compareText = compareText.mid(dot+1, offset);
	}
	if (!lookup.isEmpty() || !compareText.isEmpty()) {
		compareText = compareText.toLower();
		QStringList found;
		QStringList varList = PythonQt::self()->introspection(mContext, lookup, PythonQt::Anything);
		//QStringList funcList = PythonQt::self()->introspection(mContext, lookup, PythonQt::Function);
		/*foreach (QString n, funcList) {
			if (n.toLower().startsWith(compareText) && !n.toLower().endsWith("__")) {
				found << n+tr("(");
			}
		}*/
		foreach (QString n, varList) {
			if (n.toLower().startsWith(compareText) 
				&& !n.toLower().endsWith("__") 
				//&& !found.contains(n+tr("("))
				) {
				found << n;
			}
		}

		if (!found.isEmpty()) {
			mCompleter->setCompletionPrefix(compareText);
			mCompleter->setCompletionMode(QCompleter::PopupCompletion);
			mCompleter->setModel(new QStringListModel(found, mCompleter));
			mCompleter->setCaseSensitivity(Qt::CaseInsensitive);
			QTextCursor c = this->textCursor();
			c.movePosition(QTextCursor::StartOfWord);
			QRect cr = cursorRect(c);
			cr.setWidth(mCompleter->popup()->sizeHintForColumn(0)
				+ mCompleter->popup()->verticalScrollBar()->sizeHint().width());
			cr.translate(0,8);
			mCompleter->complete(cr);
		} else {
			mCompleter->popup()->hide();
		}
	}
	else {
		mCompleter->popup()->hide();
	}
}

void JScriptConsole::keyPressEvent(QKeyEvent* event)
{
	if (mCompleter && mCompleter->popup()->isVisible()) {
		// The following keys are forwarded by the completer to the widget
		switch (event->key()) 
		{
		case Qt::Key_Return:
			if (!mCompleter->popup()->currentIndex().isValid()) {
				insertCompletion(mCompleter->currentCompletion());
				mCompleter->popup()->hide();
				event->accept();
			}
			event->ignore();
			return;
			break;
		case Qt::Key_Enter:
		case Qt::Key_Escape:
		case Qt::Key_Tab:
			if (!mCompleter->popup()->currentIndex().isValid()) {
				insertCompletion(mCompleter->currentCompletion());
				mCompleter->popup()->hide();
				event->accept();
			}
			event->ignore();
			return;
			break;
		case Qt::Key_Backtab:

			event->ignore();
			return; // let the completer do default behavior
		default:
			break;
		}
	}
	bool eventHandled = false;
	QTextCursor textCursor = this->textCursor();

	int key = event->key();
	switch (key)
	{
	case Qt::Key_Left:

		// Moving the cursor left is limited to the position
		// of the command prompt.

		if (textCursor.position() <= commandPromptPosition()) {
			QApplication::beep();
			eventHandled = true;
		}
		break;

	case Qt::Key_Up:

		// Display the previous command in the history
		if (event->modifiers() & Qt::ShiftModifier && mHistoryPosition>0) {
			mHistoryPosition--;
			changeHistory();
		}

		eventHandled = true;
		break;

	case Qt::Key_Down:

		// Display the next command in the history
		if (event->modifiers() & Qt::ShiftModifier && mHistoryPosition+1<=mHistory.count()) {
			mHistoryPosition++;
			changeHistory();
		}

		eventHandled = true;
		break;

	case Qt::Key_Return:
		
		executeLine();
		eventHandled = true;

		break;

	case Qt::Key_Backspace:

		if (textCursor.hasSelection() && textCursor.position() <= commandPromptPosition()) {

			cut();
			eventHandled = true;

		} else {

			// Intercept backspace key event to check if
			// deleting a character is allowed. It is not
			// allowed, if the user wants to delete the
			// command prompt.

			if (textCursor.position() <= commandPromptPosition()) {

				QApplication::beep();
				eventHandled = true;
			}
		}
		break;

	case Qt::Key_Delete:

		cut();
		eventHandled = true;
		break;

	default:

		if (key >= Qt::Key_Space && key <= Qt::Key_division) {

			if (textCursor.hasSelection() && !verifySelectionBeforeDeletion()) {

				// The selection must not be deleted.
				eventHandled = true;

			} else 
			{

				// The key is an input character, check if the cursor is
				// behind the last command prompt, else inserting the
				// character is not allowed.

				int commandPromptPosition = this->commandPromptPosition();
				if (textCursor.position() < commandPromptPosition) {

					textCursor.setPosition(commandPromptPosition);
					setTextCursor(textCursor);
				}
			}
		}
	}

	if (eventHandled) {

		mCompleter->popup()->hide();
		event->accept();

	} else {

		QTextEdit::keyPressEvent(event);
		QString text = event->text();
		if (!text.isEmpty()) {
			handleCompleterMenu();
		} else {
			mCompleter->popup()->hide();
		}
		eventHandled = true;
	}
}

void JScriptConsole::cut() {

	bool deletionAllowed = verifySelectionBeforeDeletion();
	if (deletionAllowed) {
		QTextEdit::cut();
	}
}

bool JScriptConsole::verifySelectionBeforeDeletion() {

	bool deletionAllowed = true;


	QTextCursor textCursor = this->textCursor();

	int commandPromptPosition = this->commandPromptPosition();
	int selectionStart        = textCursor.selectionStart();
	int selectionEnd          = textCursor.selectionEnd();

	if (textCursor.hasSelection()) {

		// Selected text may only be deleted after the last command prompt.
		// If the selection is partly after the command prompt set the selection
		// to the part and deletion is allowed. If the selection occurs before the
		// last command prompt, then deletion is not allowed.

		if (selectionStart < commandPromptPosition ||
			selectionEnd < commandPromptPosition) {

				// Assure selectionEnd is bigger than selection start
				if (selectionStart > selectionEnd) {
					int tmp         = selectionEnd;
					selectionEnd    = selectionStart;
					selectionStart  = tmp;
				}

				if (selectionEnd < commandPromptPosition) {

					// Selection is completely before command prompt,
					// so deletion is not allowed.
					QApplication::beep();
					deletionAllowed = false;

				} else {

					// The selectionEnd is after the command prompt, so set
					// the selection start to the commandPromptPosition.
					selectionStart = commandPromptPosition;
					textCursor.setPosition(selectionStart);
					textCursor.setPosition(selectionStart, QTextCursor::KeepAnchor);
					setTextCursor(textCursor);
				}
		}

	} else { // if (hasSelectedText())

		// When there is no selected text, deletion is not allowed before the
		// command prompt.
		if (textCursor.position() < commandPromptPosition) {

			QApplication::beep();
			deletionAllowed = false;
		}
	}

	return deletionAllowed;
}

void JScriptConsole::changeHistory() {

	// Select the text after the last command prompt ...
	QTextCursor textCursor = this->textCursor();
	textCursor.movePosition(QTextCursor::End);
	textCursor.setPosition(commandPromptPosition(), QTextCursor::KeepAnchor);

	// ... and replace it with the history text.
	textCursor.insertText(mHistory.value(mHistoryPosition));

	textCursor.movePosition(QTextCursor::End);
	setTextCursor(textCursor);
}

void JScriptConsole::consoleMessage(const QString & message) {

	append(QString());
	insertPlainText(message);

	// Reset all font modifications done by the html string
	setCurrentCharFormat(mDefaultTextCharacterFormat);
}

void JScriptConsole::braceMatching()
{
	/*QTextCharFormat format;
	format.setForeground(Qt::red);
	format.setFontWeight(QFont::Bold);*/
	QTextEdit::ExtraSelection mSelection;
	QList<QTextEdit::ExtraSelection> extraSelections;
	extraSelections.clear();
	setExtraSelections(extraSelections);
	QColor lineColor = QColor(Qt::yellow).lighter(160);
	mSelection.format.setBackground(lineColor);

	QTextDocument *doc = document();
	QTextCursor cursor = textCursor();
	QTextCursor beforeCursor = cursor;

	cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor);
	QString brace = cursor.selectedText();

	beforeCursor.movePosition(QTextCursor::PreviousCharacter, QTextCursor::KeepAnchor);
	QString beforeBrace = beforeCursor.selectedText();

	if ((brace != "{") 
		&& (brace != "}") 
		&& (brace != "[") 
		&& (brace != "]") 
		&& (brace != "(")
		&& (brace != ")")) 
	{
		if ((beforeBrace == "{") 
			|| (beforeBrace == "}") 
			|| (beforeBrace == "[")
			|| (beforeBrace == "]")
			|| (beforeBrace == "(")
			|| (beforeBrace == ")")) 
		{
			cursor = beforeCursor;
			brace = cursor.selectedText();
		} else {
			return;
		}
	}

	QString openBrace;
	QString closeBrace;

	if ((brace == "{") || (brace == "}")) {
		openBrace = "{";
		closeBrace = "}";
	}

	if ((brace == "[") || (brace == "]")) {
		openBrace = "[";
		closeBrace = "]";
	}

	if ((brace == "(") || (brace == ")")) {
		openBrace = "(";
		closeBrace = ")";
	}

	if (brace == openBrace) {
		QTextCursor cursor1 = doc->find(closeBrace, cursor);
		QTextCursor cursor2 = doc->find(openBrace, cursor);
		if (cursor2.isNull()) {
			mSelection.cursor = cursor;
			extraSelections.append(mSelection);
			mSelection.cursor = cursor1;
			extraSelections.append(mSelection);
			setExtraSelections(extraSelections);
		} else {

			while (cursor1.position() > cursor2.position()) {
				cursor1 = doc->find(closeBrace, cursor1);
				cursor2 = doc->find(openBrace, cursor2);
				if (cursor2.isNull()) {
					break;
				}
			}
			mSelection.cursor = cursor;
			extraSelections.append(mSelection);
			mSelection.cursor = cursor1;
			extraSelections.append(mSelection);
			setExtraSelections(extraSelections);
		}
	} else {
		if (brace == closeBrace) {
			QTextCursor cursor1 = doc->find(openBrace, cursor, QTextDocument::FindBackward);
			QTextCursor cursor2 = doc->find(closeBrace, cursor, QTextDocument::FindBackward);
			if (cursor2.isNull()) {
				mSelection.cursor = cursor;
				extraSelections.append(mSelection);
				mSelection.cursor = cursor1;
				extraSelections.append(mSelection);
				setExtraSelections(extraSelections);
			} else {
				while (cursor1.position() < cursor2.position()) {
					cursor1 = doc->find(openBrace, cursor1, QTextDocument::FindBackward);
					cursor2 = doc->find(closeBrace, cursor2, QTextDocument::FindBackward);
					if (cursor2.isNull()) {
						break;
					}
				}
				mSelection.cursor = cursor;
				extraSelections.append(mSelection);
				mSelection.cursor = cursor1;
				extraSelections.append(mSelection);
				setExtraSelections(extraSelections);
			}
		}
	}
}

void JScriptConsole::slotCursorPositionChanged()
{
	QTextCursor _cursor = this->textCursor();
	if (_cursor.position() < commandPromptPosition())
	{
		setReadOnly(true);
		setUndoRedoEnabled(false);
	}
	else
	{
		setReadOnly(false);
		setUndoRedoEnabled(true);
	}
	if(mLineColor.isValid()) viewport()->update();
	braceMatching();
}

void JScriptConsole::paintEvent( QPaintEvent * evt)
{
	mLineColor = QColor::fromCmykF(0.05,0.05,0.05,0.0);
	QPainter painter(viewport());
	if (mLineColor.isValid())
	{
		QRect r = JScriptConsole::cursorRect();
		r.setX(2);
		r.setWidth(viewport()->width()-3);
		painter.fillRect(r, QBrush(mLineColor));
	}
	painter.end();
	QTextEdit::paintEvent(evt);
}

void JScriptConsole::slotViewportUpdate()
{
	viewport()->update();
}
